{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img src=\"../common/rfsoc_book_banner.jpg\" alt=\"University of Strathclyde\" align=\"left\">"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<div class=\"alert alert-block\" style=\"background-color: #c7b8d6; padding: 10px\">\n",
    "    <p style=\"color: #222222\">\n",
    "        <b>Note:</b>\n",
    "        <br>\n",
    "        This Jupyter notebook uses hardware features of the Zynq UltraScale+ RFSoC device. Therefore, the notebook cells will only execute successfully on an RFSoC platform.\n",
    "        <br>\n",
    "        <b>This Jupyter notebook is not compatible with the ZCU216 development board as it does not contain the SD-FEC integrated block.</b>\n",
    "    </p>\n",
    "</div>\n",
    "\n",
    "# Notebook Set H\n",
    "\n",
    "---\n",
    "\n",
    "## 05 - FEC Bit Error Analysis\n",
    "This is the fifth and final notebook exploring Soft Decision Forward Error Correction (SD-FEC) on RFSoC. In previous notebooks, we have learned how to use the SD-FEC integrated blocks on the RFSoC device to perform LDPC encoding and decoding. In addition to this, we have investigated a simplified radio pipeline that performs baseband modulation, and demodulation into soft bits after being subjected to an AWGN channel. The performance measure, BER, was introduced which provides insight into how a code performs for a given SNR in terms of bit errors. In this notebook we will bring all these components together to make multiple measurements of BER and plot BER curves which is a useful analytical tool when assessing and comparing code performance. \n",
    "\n",
    "## Table of Contents\n",
    "* [1. Introduction](#nb5_introduction)\n",
    "    * [1.1. Design Overview](#nb5_design_overview)\n",
    "    * [1.2. Notebook Setup](#nb5_notebook_setup)\n",
    "* [2. BER Curves](#nb5_ber_curves)\n",
    "    * [2.1. Initialise](#nb5_initialise)\n",
    "    * [2.2. Code Selection](#nb5_code_sel)\n",
    "    * [2.3. Transfer Data Block](#nb5_transfer_data_block)\n",
    "    * [2.4. Plot BER Curves](#nb5_plot_ber_curves)\n",
    "* [3. Hardware Acceleration](#nb5_hw_accel)\n",
    "    * [3.1. Setup](#nb5_hw_setup)\n",
    "    * [3.2. Initialise](#nb5_hw_code_selection)\n",
    "    * [3.3. Interrupts](#nb5_hw_interrupts)\n",
    "    * [3.4. Main](#nb5_hw_main)\n",
    "    * [3.5. Plot BER Curves](#nb5_hw_plot_ber)\n",
    "* [4. Conclusion](#nb5_conclusion)\n",
    "\n",
    "## Revision\n",
    "* **v1.0** | 19/01/23 | *First Revision*\n",
    "* **v1.1** | 19/05/23 | *Minor changes for new development boards (ZCU208 & ZCU216)*\n",
    "\n",
    "---\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 1. Introduction <a class=\"anchor\" id=\"nb5_introduction\"></a>\n",
    "In this notebook we will be generating BER curves to compare the performance of the LDPC codes: DOCSIS Short, DOCSIS Medium, and DOCSIS Long. By utilising the *bypass* functionality of the SD-FEC cores, we can additionally plot a BER curve for when no error correction code is in use; this will be referred to as *uncoded*. The first section of this notebook will use the same process that has been employed in the previous notebooks to obtain BER values. However, because only the encoding and decoding is performed on the programmable logic (PL) and a significant amount of processing is undergone in the processing system (PS), it will be demonstrated that this leads to very long execution times and is therefore not feasible for generating BER curves with good resolution on the x-axis and accuracy on the y-axis. Therefore, in the section two, a second design will be presented where all of the processing has been moved to the programmable logic and Jupyter is only used for control and visualisation. This results in decreased execution times. When processing is moved from the PS to the PL to decrease execution times, this is referred to as hardware acceleration. The same principals that have been explored thus far are still applicable. The companion book for these notebooks, *\"Software Defined Radio with Zynq Ultrascale+ RFSoC\"*, provides more detail on designing a simplified radio pipeline in hardware and interfacing this with the SD-FEC integrated blocks on RFSoC. \n",
    "\n",
    "### 1.1. Design Overview <a class=\"anchor\" id=\"nb5_design_overview\"></a>\n",
    "Figure 1 shows the interaction of the various components discussed thus far and illustrates how these are partitioned between the PL and PS.\n",
    "<a class=\"anchor\" id=\"fig-1\"></a>\n",
    "<center><figure>\n",
    "<img src='./images/ber_curves_design.svg' width='1000'/>\n",
    "    <figcaption><b>Figure 1: Functional block diagram illustrating the separation of components between PS and PL.</b></figcaption>\n",
    "</figure></center>\n",
    "\n",
    "Following the arrows from *Random Number Generation* through to *BER* results in one BER result for one SNR value. Therefore, this process will have to be looped numerous times, varying the SNR, over a range of LDPC codes. In the following section we will construct a series of functions to help with this process.\n",
    "\n",
    "### 1.2. Notebook Setup <a class=\"anchor\" id=\"nb5_notebook_setup\"></a>\n",
    "We must first setup the notebook by importing the required libraries and downloading the bitstream to the board."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from pynq import allocate\n",
    "import numpy as np\n",
    "import xsdfec\n",
    "import strath_sdfec.helper_functions as hf\n",
    "\n",
    "from strath_sdfec.overlay import SdfecOverlay\n",
    "ol = SdfecOverlay()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 2. BER Curves <a class=\"anchor\" id=\"nb5_ber_curves\"></a>\n",
    "In this section we will generate a series of BER curves for analysis. The pseudo code below outlines the general procedure for achieving this. \n",
    "\n",
    "1. Initialise the encoder and decoder\n",
    "    - add LDPC parameters to the SD-FEC blocks\n",
    "    - create control and status buffers for encoder and decoder\n",
    "2. Loop over LDPC codes\n",
    "    - Select code (create control word)\n",
    "    - Establish data buffers\n",
    "    3. Loop over SNRs\n",
    "        - Set channel SNR\n",
    "        - Transfer data block\n",
    "        - Calculate BER\n",
    "        - Exit loop when completed range of SNRs\n",
    "    - Exit loop when all codes completed\n",
    "\n",
    "In the following subsections, we will define a series of functions that allow us to move through these steps easily. \n",
    "    \n",
    "### 2.1. Initialise <a class=\"anchor\" id=\"nb5_initialise\"></a>\n",
    "To begin, we will initialise the encoder and decoder in the code cell below so we can use them in our functions without having to pass them as arguments. Here, we also allocate our control and status buffers for the encoder and decoder."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Initialise encoder hardware\n",
    "enc_fec = ol.ldpc_encoder.sd_fec\n",
    "enc_dma_data = ol.ldpc_encoder.axi_dma_data\n",
    "enc_dma_ctrl = ol.ldpc_encoder.axi_dma_ctrl\n",
    "enc_ldpc_params = enc_fec.available_ldpc_params()\n",
    "hf.add_all_ldpc_params(enc_fec)\n",
    "\n",
    "# Initialise decoder hardware\n",
    "dec_fec = ol.ldpc_decoder.sd_fec\n",
    "dec_dma_data = ol.ldpc_decoder.axi_dma_data\n",
    "dec_dma_ctrl = ol.ldpc_decoder.axi_dma_ctrl\n",
    "dec_ldpc_params = dec_fec.available_ldpc_params()\n",
    "hf.add_all_ldpc_params(dec_fec)\n",
    "\n",
    "# Create control/status buffers\n",
    "enc_ctrl_buffer = allocate(shape=(1,), dtype=np.uint32)\n",
    "enc_status_buffer = allocate(shape=(1,), dtype=np.uint32)\n",
    "dec_ctrl_buffer = allocate(shape=(1,), dtype=np.uint32)\n",
    "dec_status_buffer = allocate(shape=(1,), dtype=np.uint32)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2.2. Code Selection <a class=\"anchor\" id=\"nb5_code_sel\"></a>\n",
    "We want to be able to change between LDPC codes easily. As we know from previous notebooks, each code will require a different size of data buffer. Therefore, upon specifying a code, we must also allocate our tx and rx buffers for both our encoder and decoder. To change the code employed by the SD-FEC blocks, we supply a control word which indicates the code ID associated with the code. In the function below, we create the necessary control words for the SD-FEC blocks and allocate our data buffer sizes. At the top of the function, you might notice something that has not been presented before - CORE_BYPASS. This bypass functionality, when active, simply means that the SD-FEC core outputs the same data that is input to it. This allows us to obtain BER measurements for when no error correction is performed. The code name is changed in this circumstance to *docsis_medium* so that the data buffers are configured with this code’s required size, as it provides a good trade-off between speed and accuracy as you will see when generating the BER plots. The data buffers are returned by this function so that they can be used in the other functions that follow.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def get_data_buffers(code_name):\n",
    "    # Do not perform encoding/decoding when code_name is 'uncoded'\n",
    "    if code_name == 'uncoded':\n",
    "        enc_fec.CORE_BYPASS = 1\n",
    "        dec_fec.CORE_BYPASS = 1\n",
    "        code_name = 'docsis_medium'\n",
    "    else:\n",
    "        enc_fec.CORE_BYPASS = 0\n",
    "        dec_fec.CORE_BYPASS = 0\n",
    "    \n",
    "    code_id = dec_ldpc_params.index(code_name) \n",
    "\n",
    "    # Create encoder ctrl word\n",
    "    enc_ctrl_params = {'id' : 1, \n",
    "                       'code_id' : code_id}\n",
    "    enc_ctrl_word = hf.create_ctrl_word(enc_ctrl_params)\n",
    "    enc_ctrl_buffer[0] = enc_ctrl_word\n",
    "\n",
    "    # Create decoder control word\n",
    "    dec_ctrl_params = {'id' : 1, \n",
    "                       'max_iterations' : 32,\n",
    "                       'term_on_no_change' : 1,\n",
    "                       'term_on_pass' : 1, \n",
    "                       'include_parity_op' : 0,\n",
    "                       'hard_op' : 1,\n",
    "                       'code_id' : code_id}\n",
    "    dec_ctrl_word = hf.create_ctrl_word(dec_ctrl_params)\n",
    "    dec_ctrl_buffer[0] = dec_ctrl_word\n",
    "\n",
    "    n = dec_fec._code_params.ldpc[code_name]['n']\n",
    "    k = dec_fec._code_params.ldpc[code_name]['k']\n",
    "    p = dec_fec._code_params.ldpc[code_name]['p']\n",
    "    K = int(np.ceil(k/8))\n",
    "    N = int(np.ceil(n/8))\n",
    "\n",
    "    enc_tx_buf = allocate(shape=(K,), dtype=np.uint8)\n",
    "    enc_rx_buf = allocate(shape=(N,), dtype=np.uint8)\n",
    "    dec_tx_buf = allocate(shape=(n,), dtype=np.uint8)\n",
    "    dec_rx_buf = allocate(shape=(K,), dtype=np.uint8)\n",
    "    \n",
    "    data_buffers = {'enc_tx_buf' : enc_tx_buf,\n",
    "                    'enc_rx_buf' : enc_rx_buf,\n",
    "                    'dec_tx_buf' : dec_tx_buf,\n",
    "                    'dec_rx_buf' : dec_rx_buf,\n",
    "                    'n' : n}\n",
    "    return data_buffers"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2.3. Transfer Data Block <a class=\"anchor\" id=\"nb5_transfer_data_block\"></a>\n",
    "Having established the LDPC code employed by the encoder and decoder, we would now like to specify an SNR value for our channel and then perform a data transfer over that channel. The function below takes two arguments: *data_buffers* and *snr*. The first argument, *data_buffers*, is a dictionary containing four data buffers required for data movement in our design. Two for the encoder and two for the decoder. This is the output of the function defined in the previous subsection. The second argument, *snr*, allows the channel noise to be adjusted. This function follows a direct pipeline. \n",
    "1. Random data is generated (tx)\n",
    "2. Data is encoded\n",
    "3. Encoded data is baseband modulated \n",
    "4. The modulated signal is exposed to an AWGN channel with an SNR specified by *snr*.\n",
    "5. The noisy signal is baseband demodulated into soft bits.\n",
    "6. The soft bits are decoded (rx)\n",
    "7. Tx and Rx are serialised.\n",
    "8. Serialised Tx and Rx are returned by the function\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def transfer_data_block(data_buffers, snr):\n",
    "    enc_tx_buf = data_buffers['enc_tx_buf']\n",
    "    enc_rx_buf = data_buffers['enc_rx_buf']\n",
    "    dec_tx_buf = data_buffers['dec_tx_buf']\n",
    "    dec_rx_buf = data_buffers['dec_rx_buf']\n",
    "    n = data_buffers['n']\n",
    "    \n",
    "    # Data Gen.\n",
    "    for i in range(len(enc_tx_buf)):\n",
    "        enc_tx_buf[i] = np.random.randint(0,256)\n",
    "\n",
    "    # Encode\n",
    "    hf.fec_data_transfer(enc_dma_data,enc_tx_buf,enc_rx_buf,\n",
    "                         enc_dma_ctrl,enc_ctrl_buffer,enc_status_buffer)\n",
    "    encoded_data_bits = hf.serialise(enc_rx_buf)\n",
    "    encoded_data_bits = encoded_data_bits[0:n]   # Remove non-valid bits\n",
    "\n",
    "    # Simplified radio pipeline\n",
    "    signal = hf.symbol_map(encoded_data_bits)    # Baseband modulation\n",
    "    signal_with_noise = hf.awgn(signal, snr)     # Channel\n",
    "    llrs = hf.calc_llrs(signal_with_noise)       # Baseband demodulation\n",
    "\n",
    "    # Decode\n",
    "    llrs_formatted = hf.format_llrs(llrs)\n",
    "    j = 0\n",
    "    for i in range(len(dec_tx_buf)):\n",
    "        dec_tx_buf[i] = llrs_formatted[j]\n",
    "        j += 1\n",
    "    hf.fec_data_transfer(dec_dma_data,dec_tx_buf,dec_rx_buf,\n",
    "                         dec_dma_ctrl,dec_ctrl_buffer,dec_status_buffer)\n",
    "\n",
    "    tx = hf.serialise(enc_tx_buf)\n",
    "    rx = hf.serialise(dec_rx_buf)\n",
    "    return [tx,rx]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "tags": []
   },
   "source": [
    "### 2.5. Plot BER Curves <a class=\"anchor\" id=\"nb5_plot_ber_curves\"></a>\n",
    "We now have functions for updating the LDPC code and performing a data transfer over a channel that we can vary the noise of. In the following cell we will use these functions to measure multiple BERs over a range of SNRs for a number of LDPC codes. Two loops are created to allow us to vary the LDPC code (loop A) and channel SNR (loop B). To measure the BER we must:\n",
    "\n",
    "1. Set the LDPC code being used; where the encoder/decoder control words are created and data buffer sizes allocated. \n",
    "2. Set the SNR of the AWGN channel and transfer a block of data through our simplified radio pipeline.\n",
    "3. Perform a BER measurement by comparing the transmitted data and received, decoded data. \n",
    "\n",
    "Running the cell below will take approximately 3 min 30 sec to complete. Some print statements have been included to indicate the progress. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "# Specify the SNR values and LDPC codes to be looped over\n",
    "SNRs = [20,19,18,17,16,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0]\n",
    "codes = ['docsis_short','docsis_medium','docsis_long','uncoded']\n",
    "\n",
    "code_BER = []\n",
    "for code in codes:               # A. Loop over codes\n",
    "    print('\\nProgress:',code)\n",
    "    print('SNR:',end=' ')\n",
    "    BERs = []\n",
    "    for snr in SNRs:             # B. Loop over SNRs\n",
    "        print(snr,end=' ')\n",
    "        data_buffers = get_data_buffers(code)        # 1. Set code and return data buffers\n",
    "        txrx = transfer_data_block(data_buffers,snr) # 2. Set channel SNR and transfer buffers\n",
    "        # Measure BER\n",
    "        N_error = np.sum(txrx[0] != txrx[1])\n",
    "        N_bits = len(txrx[0])\n",
    "        ber = N_error / N_bits                       # 3. Measure BER\n",
    "        BERs.append(ber)\n",
    "    code_BER.append(BERs)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Using the SNR values as our x-axis, we can plot the BER measurements for each of the LDPC codes."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "hf.plot_ber('BER Curve',SNRs,code_BER,codes)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "As you may have noticed, collecting all of the BER measurements can take a considerable amount of time. Additionally, the resolution of the plots is low, as we only recorded a BER measurement at 1 dB SNR increments. The accuracy of the measured results may also be low, which is particularly evident in the case of DOCSIS Short. This is due to the fact that we are measuring over a smaller number of bits. While increasing the number of blocks transferred for each SNR value will lead to better results, it will also increase execution time. The current architecture was designed to be educational and builds much of the pipeline from first principles in Python. However, in the following section, we will introduce a new architecture that moves the entire pipeline to the RFSoC's programmable logic, significantly improving execution times.\n",
    "\n",
    "## 3. Hardware Acceleration <a class=\"anchor\" id=\"nb5_hw_accel\"></a>\n",
    "In this section we will compare the same LDPC codes using the BER metric. However, our pipeline has been hardware accelerated by moving all of the components to the programmable logic. Figure 2 illustrates the new architecture. \n",
    "<a class=\"anchor\" id=\"fig-2\"></a>\n",
    "<center><figure>\n",
    "<img src='./images/ber_curves_design_hw.svg' width='1000'/>\n",
    "    <figcaption><b>Figure 2: Functional block diagram illustrating the separation of components between PS and PL when hardware accelerated.</b></figcaption>\n",
    "</figure></center>\n",
    "\n",
    "As can be observed from the figure, the overall pipeline remains unchanged, however, there are some variations in how data is transferred between the PS and PL, as well as how the control and status words are written to and read from the SD-FEC cores. Instead of moving large blocks of data between the PS and PL, data is generated in the PL, and only control words and the BER measurement are transferred. This eliminates the need for using DMAs, as data movement can instead be accomplished using the AXI-Lite protocol. As a result, there is no requirement to create data buffers of a specific size each time the LDPC code is changed. Reducing the movement of large data blocks between the PS and PL greatly reduces the overall execution time. The control and status words required for the SD-FEC core are now handled by a custom IP, called *SD-FEC Controller*, which again removes any DMA provision. Control words are supplied to a memory-mapped address on the *SD-FEC Controller* IP core, and the core communicates the control word over AXI4-Stream to the SD-FEC core at the required rate dictated by the block size. Another important thing to note is that the *BER Calculation* block outputs an interrupt. This signals that the BER measurement has completed.\n",
    "\n",
    "### 3.1. Setup <a class=\"anchor\" id=\"nb5_hw_setup\"></a>\n",
    "We must download the new bitstream to the RFSoC. A library of IP core drivers, *sdfec_ip*, is imported to better facilitate the interaction between Python and the IP cores shown in Figure 2."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import ipywidgets as ipw\n",
    "import numpy as np\n",
    "import xsdfec\n",
    "import strath_sdfec.sdfec_ip\n",
    "import strath_sdfec.helper_functions as hf\n",
    "\n",
    "from strath_sdfec.overlay import SdfecOverlay\n",
    "ol = SdfecOverlay('hw_accel')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "As before, we again initialise our SD-FEC blocks by adding all the available parameters to each block's internal memory."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Initialise encoder hardware\n",
    "enc_fec = ol.sd_fec_enc\n",
    "enc_ldpc_params = enc_fec.available_ldpc_params()\n",
    "hf.add_all_ldpc_params(enc_fec)\n",
    "\n",
    "# Initialise decoder hardware\n",
    "dec_fec = ol.sd_fec_dec\n",
    "dec_ldpc_params = dec_fec.available_ldpc_params()\n",
    "hf.add_all_ldpc_params(dec_fec)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 3.2. Code Selection <a class=\"anchor\" id=\"nb5_hw_code_selection\"></a>\n",
    "The cell below defines a function for updating the LDPC code employed by the encoder and decoder. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def set_ldpc_code(code_name):\n",
    "    # Do not perform encoding/decoding when code_name is 'uncoded'\n",
    "    if code_name == 'uncoded':\n",
    "        enc_fec.CORE_BYPASS = 1\n",
    "        dec_fec.CORE_BYPASS = 1\n",
    "        code_name = 'docsis_medium'\n",
    "    else:\n",
    "        enc_fec.CORE_BYPASS = 0\n",
    "        dec_fec.CORE_BYPASS = 0\n",
    "    code_id = dec_ldpc_params.index(code_name)\n",
    "    print('Code ID:',code_id)\n",
    "\n",
    "    # Create encoder contol word and supply it to SD-FEC Controller IP core\n",
    "    e_ctrl_params = {'id' : 0, 'code_id' : code_id}\n",
    "    enc_ctrl = hf.create_ctrl_word(e_ctrl_params)\n",
    "    n = enc_fec._code_params.ldpc[enc_ldpc_params[code_id]]['n']\n",
    "    k = enc_fec._code_params.ldpc[enc_ldpc_params[code_id]]['k']\n",
    "    p = enc_fec._code_params.ldpc[enc_ldpc_params[code_id]]['p']\n",
    "    print('Encoder (n,k,p): ', n, k, p)\n",
    "    enc_length = int(k / 8)\n",
    "    ol.fec_ctrl_enc.set(enc_ctrl, np.ceil(enc_length))\n",
    "\n",
    "    # Create decoder contol word and supply it to SD-FEC Controller IP core\n",
    "    d_ctrl_params = {'id' : 0, \n",
    "                     'max_iterations' : 32,\n",
    "                     'term_on_no_change' : 1,\n",
    "                     'term_on_pass' : 1, \n",
    "                     'include_parity_op' : 0,\n",
    "                     'hard_op' : 1,\n",
    "                     'code_id' : code_id}\n",
    "    dec_ctrl = hf.create_ctrl_word(d_ctrl_params)\n",
    "    n = dec_fec._code_params.ldpc[dec_ldpc_params[code_id]]['n']\n",
    "    k = dec_fec._code_params.ldpc[dec_ldpc_params[code_id]]['k']\n",
    "    p = dec_fec._code_params.ldpc[dec_ldpc_params[code_id]]['p']\n",
    "    print('Decoder (n,k,p): ', n, k, p)\n",
    "    dec_length = n / 4\n",
    "    ol.fec_ctrl_dec.set(dec_ctrl, np.ceil(dec_length))\n",
    "\n",
    "    # Configure the QAM Mapper to remove any non-valid bits\n",
    "    ol.qam_mapping.setup(n)\n",
    "    \n",
    "    # Return code name used\n",
    "    if enc_fec.CORE_BYPASS:\n",
    "        return 'uncoded'\n",
    "    else:\n",
    "        return enc_ldpc_params[code_id] + ' / ' + dec_ldpc_params[code_id]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 3.3. Interrupts <a class=\"anchor\" id=\"nb5_hw_interrupts\"></a>\n",
    "The *BER Calculation* IP core produces an interrupt when a measurement has been completed over *N* blocks. The cell below defines an interrupt class that uses a separate thread to wait for the interrupt returned by the *BER Calculation* IP core (*irq=ol.ber.ber_intr*) and then execute the callback associated with the interrupt (*irq_callback=intr_handler*). Our callback or interrupt handler in this instance simply sets an interrupt flag high for use in the *main* loop detailed in the next subsection."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import asyncio\n",
    "import threading\n",
    "\n",
    "class Interrupt():  \n",
    "    def __init__(self, \n",
    "                 irq, \n",
    "                 irq_callback): \n",
    "        self._interrupt = irq\n",
    "        self._irq_callback = irq_callback\n",
    "        self._loop = asyncio.get_event_loop()\n",
    "        self.is_running = False\n",
    "        self.thread = None\n",
    "        \n",
    "    async def _wait(self):\n",
    "        await self._interrupt.wait() # Wait for IRQ level\n",
    "        self._irq_callback()\n",
    "        \n",
    "    def _do(self):\n",
    "        while True:\n",
    "            self.is_running = True\n",
    "            future = asyncio.run_coroutine_threadsafe(self._wait(), self._loop)\n",
    "            future.result()\n",
    "        \n",
    "    def start(self):\n",
    "        \"\"\"Start the async irq routine.\"\"\"\n",
    "        self.thread = threading.Thread(target=self._do)\n",
    "        self.thread.start()\n",
    "        \n",
    "def intr_handler():\n",
    "    global intr_flag\n",
    "    intr_flag = 1"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 3.4. Main <a class=\"anchor\" id=\"nb5_hw_main\"></a>\n",
    "Here, we define our main loop that will be executed on a separate thread to the interrupt routine. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def progress_bar(title, L):\n",
    "    layout = ipw.Layout(width='auto', height='40px')\n",
    "    style = {'description_width': '300px'}\n",
    "    progress_bar = ipw.IntProgress(min=0, \n",
    "                                 max=L,\n",
    "                                 description=title,\n",
    "                                 layout=layout,\n",
    "                                 style=style)\n",
    "    display(progress_bar)\n",
    "    return progress_bar\n",
    "\n",
    "# Main\n",
    "def main():\n",
    "    global intr_flag\n",
    "    \n",
    "    # Create Progress Bars\n",
    "    codes_progress = progress_bar('Codes Progress: ', len(codes))\n",
    "    code_name = set_ldpc_code(codes[0])\n",
    "    snr_progress = progress_bar('%s: ' % code_name, len(SNRs))\n",
    "    \n",
    "    # Initialise IP cores\n",
    "    ol.random_number_gen.set_signal_length(ol.fec_ctrl_enc.len * num_blocks * 8)\n",
    "    ol.awgn_channel.set_snr(SNRs[0])\n",
    "    ol.soft_demodulation.set_scaling(ol.awgn_channel.noise_var, 1)\n",
    "    ol.ber.length = ol.fec_ctrl_enc.len * num_blocks\n",
    "    \n",
    "    BER = []\n",
    "    # Run\n",
    "    ol.random_number_gen.start()\n",
    "    while(1):\n",
    "        if intr_flag == 1:\n",
    "            intr_flag = 0\n",
    "            \n",
    "            # Get BER measurement\n",
    "            ber = ol.ber.get_ber()\n",
    "            BER.append(ber)\n",
    "            ol.ber.reset()\n",
    "            \n",
    "            # B. Loop over SNRs\n",
    "            snr_progress.value += 1    \n",
    "            if snr_progress.value == len(SNRs):\n",
    "                BERs.append(BER)\n",
    "                BER = []\n",
    "                # Break out loop when the final measurement has been recorded for final code\n",
    "                if codes_progress.value == len(codes)-1:\n",
    "                    snr_progress.style.bar_color = 'green'\n",
    "                    codes_progress.value += 1\n",
    "                    codes_progress.style.bar_color = 'green'\n",
    "                    break\n",
    "                # A. Loop over codes\n",
    "                else:\n",
    "                    codes_progress.value += 1\n",
    "                    snr_progress.style.bar_color = 'green'\n",
    "                    code_name = set_ldpc_code(codes[codes_progress.value])\n",
    "                    snr_progress = progress_bar('%s: ' % code_name, len(SNRs))\n",
    "                    ol.random_number_gen.set_signal_length(ol.fec_ctrl_enc.len * num_blocks * 8)\n",
    "            \n",
    "            # Set next SNR value\n",
    "            snr = SNRs[snr_progress.value]\n",
    "            ol.awgn_channel.set_snr(snr)\n",
    "            ol.soft_demodulation.set_scaling(ol.awgn_channel.noise_var, 1)\n",
    "\n",
    "            # Start next signal to be measured\n",
    "            # (this will result in the interrupt triggering after a period)\n",
    "            ol.ber.length = ol.fec_ctrl_enc.len * num_blocks\n",
    "            ol.random_number_gen.start()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 3.5. Plot BER Curves <a class=\"anchor\" id=\"nb5_hw_plot_ber\"></a>\n",
    "Using the new functions and hardware design, we can obtain BER results that have been measured over many more bits, improving accuracy. We can also increment the SNR value by a smaller amount, improving the x-axis resolution. In the cell below we transfer 10000 blocks of data and vary the SNR by 0.2 dB, meaning for each curve we are measuring over 50000 times as many bits as were measured over for the same code in the previous implementation. The cell should take approximately 2 minutes to execute meaning despite the increased number of blocks, our execution time is actually lower. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Set Parameters\n",
    "num_blocks = 10000\n",
    "codes = ['docsis_short','docsis_medium','docsis_long','uncoded']\n",
    "SNRs = [x / 10.0 for x in range(200, 0, -2)]\n",
    "BERs = []\n",
    "\n",
    "# Start\n",
    "intr_flag = 0\n",
    "intr = Interrupt(irq=ol.ber.ber_intr,irq_callback=intr_handler)\n",
    "intr.start()\n",
    "main_thread = threading.Thread(target=main)\n",
    "main_thread.start()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Plotting the results, it can be seen that the BER curves are much smoother and we are able to better compare the code performance. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "hf.plot_ber('BER Curve',SNRs,BERs,codes)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 3. Conclusion <a class=\"anchor\" id=\"nb5_conclusion\"></a>\n",
    "In this final notebook, we have brought all the concepts and techniques covered in previous notebooks together to generate BER curves and compare the performance of different LDPC codes. We have explored the process of generating BER curves using both a PS-based and a PL-based design, and highlighted the benefits of using hardware acceleration to improve execution times. Overall, this series has provided a comprehensive overview of how to utilise the SD-FEC core on RFSoC for performing LDPC coding and decoding and analyzing the performance of different codes. By the end of this series, readers should have a solid understanding of how to implement SD-FEC on RFSoC, and be equipped with the necessary knowledge to explore and experiment with different codes and configurations."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "[⬅️ Previous Notebook](04_fec_decoding.ipynb) || [Next Notebook 🚀](../notebook_I/01_ofdm_fundamentals.ipynb)\n",
    "\n",
    "Copyright © 2023 Strathclyde Academic Media\n",
    "\n",
    "---\n",
    "---"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.8.2"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
